2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "ESPurpleJabberAccount.h"
18 #import <AdiumLibpurple/SLPurpleCocoaAdapter.h>
19 #import <Adium/AIAccountControllerProtocol.h>
20 #import <Adium/AIInterfaceControllerProtocol.h>
21 #import <Adium/AIStatusControllerProtocol.h>
22 #import <Adium/AIContactControllerProtocol.h>
23 #import <Adium/AIChat.h>
24 #import <Adium/AIHTMLDecoder.h>
25 #import <Adium/AIListContact.h>
26 #import <Adium/AIStatus.h>
27 #import <Adium/ESFileTransfer.h>
28 #import <Adium/ESTextAndButtonsWindowController.h>
29 #import <AIUtilities/AIAttributedStringAdditions.h>
30 #include <Libpurple/buddy.h>
31 #include <Libpurple/presence.h>
32 #include <Libpurple/si.h>
34 #define DEFAULT_JABBER_HOST @"@jabber.org"
36 extern void jabber_roster_request(JabberStream *js);
38 @implementation ESPurpleJabberAccount
41 * @brief The UID will be changed. The account has a chance to perform modifications
43 * Upgrade old Jabber accounts stored with the host in a separate key to have the right UID, in the form
46 * Append @jabber.org to a proposed UID which has no domain name and does not need to be updated.
48 * @param proposedUID The proposed, pre-filtered UID (filtered means it has no characters invalid for this servce)
49 * @result The UID to use; the default implementation just returns proposedUID.
51 - (NSString *)accountWillSetUID:(NSString *)proposedUID
53 proposedUID = [proposedUID lowercaseString];
56 if ((proposedUID && ([proposedUID length] > 0)) &&
57 ([proposedUID rangeOfString:@"@"].location == NSNotFound)) {
60 //Upgrade code: grab a previously specified Jabber host
61 if ((host = [self preferenceForKey:@"Jabber:Host" group:GROUP_ACCOUNT_STATUS ignoreInheritedValues:YES])) {
62 //Determine our new, full UID
63 correctUID = [NSString stringWithFormat:@"%@@%@",proposedUID, host];
65 //Clear the preference and then set the UID so we don't perform this upgrade again
66 [self setPreference:nil forKey:@"Jabber:Host" group:GROUP_ACCOUNT_STATUS];
67 [self setPreference:correctUID forKey:@"FormattedUID" group:GROUP_ACCOUNT_STATUS];
70 //Append [self serverSuffix] (e.g. @jabber.org) to a Jabber account with no server
71 correctUID = [proposedUID stringByAppendingString:[self serverSuffix]];
74 correctUID = proposedUID;
80 - (const char*)protocolPlugin
85 - (NSSet *)supportedPropertyKeys
87 static NSMutableSet *supportedPropertyKeys = nil;
89 if (!supportedPropertyKeys) {
90 supportedPropertyKeys = [[NSMutableSet alloc] initWithObjects:
94 [supportedPropertyKeys unionSet:[super supportedPropertyKeys]];
97 return supportedPropertyKeys;
100 - (void)configurePurpleAccount
102 [super configurePurpleAccount];
104 NSString *connectServer;
105 BOOL forceOldSSL, allowPlaintext;
107 purple_account_set_username(account, [self purpleAccountName]);
109 //'Connect via' server (nil by default)
110 connectServer = [self preferenceForKey:KEY_JABBER_CONNECT_SERVER group:GROUP_ACCOUNT_STATUS];
111 //XXX - As of libpurple 2.0.0, 'localhost' doesn't work properly by 127.0.0.1 does. Hack!
112 if (connectServer && [connectServer isEqualToString:@"localhost"])
113 connectServer = @"127.0.0.1";
115 purple_account_set_string(account, "connect_server", (connectServer ?
116 [connectServer UTF8String] :
119 //Force old SSL usage? (off by default)
120 forceOldSSL = [[self preferenceForKey:KEY_JABBER_FORCE_OLD_SSL group:GROUP_ACCOUNT_STATUS] boolValue];
121 purple_account_set_bool(account, "old_ssl", forceOldSSL);
123 //Allow plaintext authorization over an unencrypted connection? Purple will prompt if this is NO and is needed.
124 allowPlaintext = [[self preferenceForKey:KEY_JABBER_ALLOW_PLAINTEXT group:GROUP_ACCOUNT_STATUS] boolValue];
125 purple_account_set_bool(account, "auth_plain_in_clear", allowPlaintext);
128 - (NSString *)serverSuffix
130 AILog(@"using jabber");
131 return DEFAULT_JABBER_HOST;
134 /*! @brief Obtain the resource name for this Jabber account.
136 * This could be extended in the future to perform keyword substitution (e.g. s/%computerName%/CSCopyMachineName()/).
138 * @return The resource name for the account.
140 - (NSString *)resourceName
142 return [self preferenceForKey:KEY_JABBER_RESOURCE group:GROUP_ACCOUNT_STATUS];
145 - (const char *)purpleAccountName
147 NSString *userNameWithHost = nil, *completeUserName = nil;
148 BOOL serverAppendedToUID;
151 * Purple stores the username in the format username@server/resource. We need to pass it a username in this format
153 * The user should put the username in username@server format, which is common for Jabber. If the user does
154 * not specify the server, use jabber.org.
157 serverAppendedToUID = ([UID rangeOfString:@"@"].location != NSNotFound);
159 if (serverAppendedToUID) {
160 userNameWithHost = UID;
162 userNameWithHost = [UID stringByAppendingString:[self serverSuffix]];
165 completeUserName = [NSString stringWithFormat:@"%@/%@" ,userNameWithHost, [self resourceName]];
167 return [completeUserName UTF8String];
171 * @brief Connect Host
173 * Convenience method for retrieving the connect host for this account
175 * Rather than having a separate server field, Jabber uses the servername after the user name.
176 * username@server.org
178 * The connect server, stored in KEY_JABBER_CONNECT_SERVER, overrides this to provide the connect host. It will
179 * not be set in most cases.
185 if (!(host = [self preferenceForKey:KEY_JABBER_CONNECT_SERVER group:GROUP_ACCOUNT_STATUS])) {
186 int location = [UID rangeOfString:@"@"].location;
188 if ((location != NSNotFound) && (location + 1 < [UID length])) {
189 host = [UID substringFromIndex:(location + 1)];
192 host = [self serverSuffix];
200 * @brief Should set aliases serverside?
202 * Jabber supports serverside aliases.
204 - (BOOL)shouldSetAliasesServerside
209 - (AIListContact *)contactWithUID:(NSString *)sourceUID
211 AIListContact *contact;
213 contact = [[adium contactController] existingContactWithService:service
217 contact = [[adium contactController] contactWithService:[self _serviceForUID:sourceUID]
225 - (AIService *)_serviceForUID:(NSString *)contactUID
227 AIService *contactService;
228 NSString *contactServiceID = nil;
230 if ([contactUID hasSuffix:@"@gmail.com"] ||
231 [contactUID hasSuffix:@"@googlemail.com"]) {
232 contactServiceID = @"libpurple-jabber-gtalk";
234 } else if([contactUID hasSuffix:@"@livejournal.com"]){
235 contactServiceID = @"libpurple-jabber-livejournal";
238 contactServiceID = @"libpurple-Jabber";
241 contactService = [[adium accountController] serviceWithUniqueID:contactServiceID];
243 return contactService;
246 #pragma mark Contacts
247 - (void)updateSignon:(AIListContact *)theContact withData:(void *)data
249 [super updateSignon:theContact withData:data];
251 //We only get user icons in Jabber when we request info. Do that now!
252 [self delayedUpdateContactStatus:theContact];
257 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forListObject:(AIListObject *)inListObject
259 static AIHTMLDecoder *jabberHtmlEncoder = nil;
260 if (!jabberHtmlEncoder) {
261 jabberHtmlEncoder = [[AIHTMLDecoder alloc] init];
262 [jabberHtmlEncoder setIncludesHeaders:NO];
263 [jabberHtmlEncoder setIncludesFontTags:YES];
264 [jabberHtmlEncoder setClosesFontTags:YES];
265 [jabberHtmlEncoder setIncludesStyleTags:YES];
266 [jabberHtmlEncoder setIncludesColorTags:YES];
267 [jabberHtmlEncoder setEncodesNonASCII:NO];
268 [jabberHtmlEncoder setPreservesAllSpaces:NO];
269 [jabberHtmlEncoder setUsesAttachmentTextEquivalents:YES];
272 return [jabberHtmlEncoder encodeHTML:inAttributedString imagesPath:nil];
275 - (NSString *)_UIDForAddingObject:(AIListContact *)object
277 NSString *objectUID = [object UID];
280 if ([objectUID rangeOfString:@"@"].location != NSNotFound) {
281 properUID = objectUID;
283 properUID = [NSString stringWithFormat:@"%@@%@",objectUID,[self host]];
286 return [properUID lowercaseString];
289 - (NSString *)unknownGroupName {
290 return (AILocalizedString(@"Roster","Roster - the Jabber default group"));
293 - (NSString *)connectionStringForStep:(int)step
297 return AILocalizedString(@"Connecting",nil);
300 return AILocalizedString(@"Initializing Stream",nil);
303 return AILocalizedString(@"Reading data",nil);
306 return AILocalizedString(@"Authenticating",nil);
309 return AILocalizedString(@"Initializing Stream",nil);
312 return AILocalizedString(@"Authenticating",nil);
318 - (BOOL)shouldAttemptReconnectAfterDisconnectionError:(NSString **)disconnectionError
320 BOOL shouldReconnect = YES;
322 if (disconnectionError && *disconnectionError) {
323 if (([*disconnectionError rangeOfString:@"401"].location != NSNotFound) ||
324 ([*disconnectionError rangeOfString:@"Authentication Failure"].location != NSNotFound) ||
325 ([*disconnectionError rangeOfString:@"Not Authorized"].location != NSNotFound)) {
326 shouldReconnect = NO;
328 /* Automatic registration attempt */
329 //Display no error message
330 [*disconnectionError release];
331 *disconnectionError = nil;
333 [[adium interfaceController] displayQuestion:AILocalizedString(@"Would you like to register a new Jabber account?", nil)
334 withDescription:AILocalizedString(@"Jabber was unable to connect due to an invalid Jabber ID or password. This may be because you do not yet have an account on this Jabber server. Would you like to register now?",nil)
335 withWindowTitle:AILocalizedString(@"Invalid Jabber ID or Password",nil)
336 defaultButton:AILocalizedString(@"Register",nil)
337 alternateButton:AILocalizedString(@"Cancel",nil)
340 selector:@selector(answeredShouldReigsterNewJabberAccount:userInfo:)
343 } else if ([*disconnectionError rangeOfString:@"Stream Error"].location != NSNotFound) {
344 shouldReconnect = NO;
346 } else if ([*disconnectionError rangeOfString:@"requires plaintext authentication over an unencrypted stream"].location != NSNotFound) {
347 shouldReconnect = NO;
349 } else if ([*disconnectionError rangeOfString:@"Resource Conflict"].location != NSNotFound) {
350 shouldReconnect = NO;
354 return shouldReconnect;
357 - (BOOL)answeredShouldReigsterNewJabberAccount:(NSNumber *)returnCodeNumber userInfo:(id)userInfo
359 AITextAndButtonsReturnCode returnCode = [returnCodeNumber intValue];
361 switch (returnCode) {
362 case AITextAndButtonsDefaultReturn:
363 [self performSelector:@selector(performRegisterWithPassword:)
368 case AITextAndButtonsAlternateReturn:
369 case AITextAndButtonsOtherReturn:
370 case AITextAndButtonsClosedWithoutResponse:
371 [self serverReportedInvalidPassword];
378 - (void)disconnectFromDroppedNetworkConnection
380 /* Before we disconnect from a dropped network connection, set gc->disconnect_timeout to a non-0 value.
381 * This will let the prpl know that we are disconnecting with no backing ssl connection and that therefore
382 * the ssl connection is has should not be messaged in the process of disconnecting.
384 PurpleConnection *gc = purple_account_get_connection(account);
385 if (PURPLE_CONNECTION_IS_VALID(gc) &&
386 !gc->disconnect_timeout) {
387 gc->disconnect_timeout = -1;
388 AILog(@"%@: Disconnecting from a dropped network connection", self);
391 [super disconnectFromDroppedNetworkConnection];
394 #pragma mark File transfer
395 - (BOOL)canSendFolders
400 - (void)beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
402 [super _beginSendOfFileTransfer:fileTransfer];
405 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
407 [super acceptFileTransferRequest:fileTransfer];
410 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
412 [super rejectFileReceiveRequest:fileTransfer];
415 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
417 [super cancelFileTransfer:fileTransfer];
420 #pragma mark Status Messages
421 - (NSAttributedString *)statusMessageForPurpleBuddy:(PurpleBuddy *)b
423 NSAttributedString *statusMessage = nil;
425 if (purple_account_is_connected(account)) {
426 char *normalized = g_strdup(purple_normalize(b->account, b->name));
429 if ((jb = jabber_buddy_find(account->gc->proto_data, normalized, FALSE))) {
430 NSString *statusMessageString = nil;
431 const char *msg = jabber_buddy_get_status_msg(jb);
434 //Get the custom jabber status message if one is set
435 statusMessageString = [NSString stringWithUTF8String:msg];
438 if (statusMessageString && [statusMessageString length]) {
439 statusMessage = [AIHTMLDecoder decodeHTML:statusMessageString];
446 return statusMessage;
449 - (NSString *)statusNameForPurpleBuddy:(PurpleBuddy *)buddy
451 NSString *statusName = nil;
452 PurplePresence *presence = purple_buddy_get_presence(buddy);
453 PurpleStatus *status = purple_presence_get_active_status(presence);
454 const char *purpleStatusID = purple_status_get_id(status);
456 if (!purpleStatusID) return nil;
458 if (!strcmp(purpleStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_CHAT))) {
459 statusName = STATUS_NAME_FREE_FOR_CHAT;
461 } else if (!strcmp(purpleStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_XA))) {
462 statusName = STATUS_NAME_EXTENDED_AWAY;
464 } else if (!strcmp(purpleStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_DND))) {
465 statusName = STATUS_NAME_DND;
473 * @brief Jabber status messages are plaintext
475 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forStatusState:(AIStatus *)statusState
477 return [[inAttributedString attributedStringByConvertingLinksToStrings] string];
480 #pragma mark Menu items
481 - (NSString *)titleForContactMenuLabel:(const char *)label forContact:(AIListContact *)inContact
483 if (strcmp(label, "Un-hide From") == 0) {
484 return [NSString stringWithFormat:AILocalizedString(@"Un-hide From %@",nil),[inContact formattedUID]];
486 } else if (strcmp(label, "Temporarily Hide From") == 0) {
487 return [NSString stringWithFormat:AILocalizedString(@"Temporarily Hide From %@",nil),[inContact formattedUID]];
489 } else if (strcmp(label, "Unsubscribe") == 0) {
490 return [NSString stringWithFormat:AILocalizedString(@"Unsubscribe %@",nil),[inContact formattedUID]];
492 } else if (strcmp(label, "(Re-)Request authorization") == 0) {
493 return [NSString stringWithFormat:AILocalizedString(@"Re-request Authorization from %@",nil),[inContact formattedUID]];
495 } else if (strcmp(label, "Cancel Presence Notification") == 0) {
496 return [NSString stringWithFormat:AILocalizedString(@"Cancel Presence Notification to %@",nil),[inContact formattedUID]];
499 return [super titleForContactMenuLabel:label forContact:inContact];
502 #pragma mark Multiuser chat
504 //Multiuser chats come in with just the contact's name as contactName, but we want to actually do it right.
505 - (NSString *)uidForContactWithUID:(NSString *)inUID inChat:(AIChat *)chat
507 return [NSString stringWithFormat:@"%@/%@",[chat name],inUID];
512 * @brief Return the purple status type to be used for a status
514 * Most subclasses should override this method; these generic values may be appropriate for others.
516 * Active services provided nonlocalized status names. An AIStatus is passed to this method along with a pointer
517 * to the status message. This method should handle any status whose statusNname this service set as well as any statusName
518 * defined in AIStatusController.h (which will correspond to the services handled by Adium by default).
519 * It should also handle a status name not specified in either of these places with a sane default, most likely by loooking at
520 * [statusState statusType] for a general idea of the status's type.
522 * @param statusState The status for which to find the purple status ID
523 * @param arguments Prpl-specific arguments which will be passed with the state. Message is handled automatically.
525 * @result The purple status ID
527 - (const char *)purpleStatusIDForStatus:(AIStatus *)statusState
528 arguments:(NSMutableDictionary *)arguments
530 const char *statusID = NULL;
531 NSString *statusName = [statusState statusName];
532 NSString *statusMessageString = [statusState statusMessageString];
533 NSNumber *priority = nil;
535 if (!statusMessageString) statusMessageString = @"";
537 switch ([statusState statusType]) {
538 case AIAvailableStatusType:
540 if (([statusName isEqualToString:STATUS_NAME_FREE_FOR_CHAT]) ||
541 ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_FREE_FOR_CHAT]] == NSOrderedSame))
542 statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_CHAT);
543 priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AVAILABLE group:GROUP_ACCOUNT_STATUS];
547 case AIAwayStatusType:
549 if (([statusName isEqualToString:STATUS_NAME_DND]) ||
550 ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_DND]] == NSOrderedSame))
551 statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_DND);
552 else if (([statusName isEqualToString:STATUS_NAME_EXTENDED_AWAY]) ||
553 ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_EXTENDED_AWAY]] == NSOrderedSame))
554 statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_XA);
555 priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AWAY group:GROUP_ACCOUNT_STATUS];
559 case AIInvisibleStatusType:
560 AILog(@"Warning: Invisibility is not yet supported in libpurple 2.0.0 jabber");
561 priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AWAY group:GROUP_ACCOUNT_STATUS];
562 statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_AWAY);
563 // statusID = "Invisible";
566 case AIOfflineStatusType:
570 //Set our priority, which is actually set along with the status...Default is 0.
571 [arguments setObject:(priority ? priority : [NSNumber numberWithInt:0])
574 //If we didn't get a purple status ID, request one from super
575 if (statusID == NULL) statusID = [super purpleStatusIDForStatus:statusState arguments:arguments];
580 #pragma mark Account Action Menu Items
581 - (NSString *)titleForAccountActionMenuLabel:(const char *)label
583 /* XXX All Jabber account actions depend upon adiumPurpleRequestFields */